package br.ufrrj.im.redes.server;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.TreeMap;


import br.ufrrj.im.redes.model.AuthenticationMessage;
import br.ufrrj.im.redes.model.Message;
import br.ufrrj.im.redes.model.MessageClosingConnection;
import br.ufrrj.im.redes.model.UpdateGameMessageForGamer;
import br.ufrrj.im.redes.model.UpdateGameMessageForSpectator;
import br.ufrrj.im.redes.model.User;
import br.ufrrj.im.redes.util.Constants;

public class Server {
	
	private int rangePrime;
	private boolean gameStarted;
	private boolean connectedServer;
	
	private ServerSocket authenticationSocket;
	private Thread threadAuhtentication;
	
	//user.getName() -> User
	//Lista de clientes conectados
	private TreeMap<String, User> avaibleUsers;
	
	//flag para que diz se houve mudanças ou não na lista de usuários disponíveis
	private boolean changedAvaibleUsers;
	
	
	private TreeMap<String, PersistentConnectionServer> avaibleUserConnections;
	private TreeMap<String, Thread> avaibleUsersConnectionsThreads;
	
	//user.getName() -> prime
	private TreeMap<String, Long> definedPrimes;
	
	

	public Server(int rangePrime) throws IOException {
		
		// TODO Auto-generated constructor stub
		this.rangePrime = rangePrime;
		this.gameStarted = false;
		
		avaibleUsers = new TreeMap<String, User>();
		
		//instância socket do servidor
		authenticationSocket = new ServerSocket(Constants.PORT_REQUEST_CONNECTION);
		
		
		//lista de números primos selecionados pelos usuários
		this.definedPrimes = new TreeMap<String, Long>();
		
		//cria uma lista de persistentConnections de cada usuário
		avaibleUserConnections = new TreeMap<String, PersistentConnectionServer>();
		
		//cria uma lista de Threads relacionados à persistentConnections de cada usuário
		avaibleUsersConnectionsThreads = new TreeMap<String, Thread>();
		
		
		//atualiza flag do estado de conexão do servidor
		this.connectedServer = true;
		
		//atualiza flag de mudança na lista de usuários disponíveis
		this.changedAvaibleUsers = false;


		System.out.println("Servidor inicializado com sucesso. Aguardando conexões...");
	}

	
	public int requestAuthentication(User user){
		
		
		//Se o jogo já foi inicializado e a autenticação é de um jogador
		if((gameStarted) && (user.getMode() == Constants.GAMER_MODE))
			return Constants.AUTHENTICATION_GAME_STARTED;

		//Se já existe um cliente conectado com este nome de usuário
		if(avaibleUsers.get(user.getName()) != null)
			return Constants.AUTHENTICATION_USER_INVALID;
		

		//Senão, adiciona o nome usuário a lista de cliente conectados.
		avaibleUsers.put(user.getName(), user);
		
		System.out.println("Usuário: " + user.getName() + " se autenticou com sucesso.");
		
		return Constants.AUTHENTICATION_ACCEPT;
		
	}
	
	public void reciveConnection(){
			
		threadAuhtentication = new Thread(new Runnable() {
			
			@Override
			public void run() {
				// TODO Auto-generated method stub
				
				Socket authenticationRequestSocket;
				ObjectInputStream msgInput;
				ObjectOutputStream msgOutput;
				AuthenticationMessage authenticationMessage;
				User user;
				int codeAuthentication;
				
				while(!authenticationSocket.isClosed()){
									
					
					try {
						 
						authenticationRequestSocket = authenticationSocket.accept();
						msgInput = new ObjectInputStream(authenticationRequestSocket.getInputStream());
						
						//valida a mensagem de autenticação
						authenticationMessage = validateAuthenticationMsg(msgInput.readObject());
						
						//obtém o usuário
						user = authenticationMessage.getUserFrom();
						user.setIp(authenticationRequestSocket.getInetAddress().getHostAddress().toString());
						
						//requisita a autenticação
						codeAuthentication = requestAuthentication(user);
						
						//cria a mensagem de resposta a requisição de autenticação e a envia para o usuário
						msgOutput = new ObjectOutputStream(authenticationRequestSocket.getOutputStream());
						msgOutput.writeObject(new AuthenticationMessage(codeAuthentication));
												
						
						//cria conexões de jogo para cada usuário (espectador ou jogador)
						if(codeAuthentication == Constants.AUTHENTICATION_ACCEPT){
							//atualiza flag de mudança na lista de usuários disponíveis
							changedAvaibleUsers = true;
							createPersistentConnectionServer(user);
						}
						
						//depois de feita a solicitação de conexão do clinte com o servidor 
						//fecha fluxos de mensagem e socket de autenticação deste cliente
						msgInput.close();
						msgOutput.close();
						authenticationRequestSocket.close();
					
						
					} catch (IOException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
						
					} catch (ClassNotFoundException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}				
				}
				
			}
		});
		
		threadAuhtentication.start();		
	}

	private void createPersistentConnectionServer(User user){
		
		PersistentConnectionServer persistentConnection;
		Thread persistentConnectionThread;	
		
		//instância a conexão persistente;
		persistentConnection = new PersistentConnectionServer(user, this);
				
		//adiciona na lista de connections a connection do usuário referente a iteração
		avaibleUserConnections.put(user.getName(), persistentConnection);
		
		//inicializa a thread da persistentConnection
		persistentConnectionThread = new Thread(persistentConnection);
		
		//adiciona na lista de connectionsThread a thread referente a conexão do usuário
		avaibleUsersConnectionsThreads.put(user.getName(), persistentConnectionThread);
				
		//inicalia a thread
		persistentConnectionThread.start();
	}
	
	
	//encerra a conexão do servidor, para servidor
	@SuppressWarnings("deprecation")
	public void stopServer() throws IOException{
		
		//Define flag de jogo inicializado como falso
		this.gameStarted = false;
		
		sendMessageToAllUsers(new MessageClosingConnection());
						
		//encerra conexões persistentes
		closePersistentConnections();
		
		//fecha socket de recebimento de solicitação de autenticação
		authenticationSocket.close();	
		
		//encerra thread de solicitação de autenticações
		threadAuhtentication.stop();
		
		//atualiza flag do estado de conexão do servidor
		this.connectedServer = false;
	}
	
	
	//encerra todas as conexões persistentes estabelecidas com o servidor, juntamente com suas respectivas threads
	@SuppressWarnings("deprecation")
	public void closePersistentConnections() throws IOException{
		
		PersistentConnectionServer persistentConnections;
		String userName;
		Thread persistentConnectionsThread;
				
		
		//encerra as conexões persistentes estabelecidas com o servidor
		while (!avaibleUserConnections.isEmpty()) {
			persistentConnections = avaibleUserConnections.values().iterator().next();
			userName = avaibleUserConnections.keySet().iterator().next();
					
			//encerra as conexões (sockets) e streams abertos
			persistentConnections.closeConnection();
			
			//Remove conexão persistente da lista de conexões
			avaibleUserConnections.remove(userName);
			//Remove o usuário da lista de usuários disponíveis
			removeUserOfListAvaibleUsers(userName);
		}
		
		
		//encerra threads referentes a conexões persistentes estabelecidas com o servidor
		while (!avaibleUsersConnectionsThreads.isEmpty()) {
			persistentConnectionsThread = avaibleUsersConnectionsThreads.values().iterator().next();
			userName = avaibleUsersConnectionsThreads.keySet().iterator().next();
			
			//encerra a thread
			persistentConnectionsThread.stop();
			
			//Remove thread da lista de threads de conexões persistentes
			avaibleUsersConnectionsThreads.remove(userName);
		}	
	}
	
	//remove o usuário passado como parâmetro, atualiza lista de usuário conectados
	public void removeUserOfListAvaibleUsers(String userName){
		
		if(this.getAvaibleUsers().get(userName) != null)
		{
			this.getAvaibleUsers().remove(userName);
			
			//atualiza flag de mudança na lista de usuários disponíveis
			this.changedAvaibleUsers = true;
		}
	}
	
		
	private AuthenticationMessage validateAuthenticationMsg(Object msg){
		
		if (msg instanceof AuthenticationMessage) {
			return (AuthenticationMessage) msg;
		}
		
		return null;
	}
	
	public void sendMessageToAllUsers(Message message) throws IOException{
		
		for (String userName : avaibleUserConnections.keySet()) {
			System.out.println("sending message of closing connection to user: " + userName);
			avaibleUserConnections.get(userName).sendMessage(message);
		}
	}

	
	public void sendUpdateToAllGamers(UpdateGameMessageForGamer message) throws IOException{
		
		User user;
		
		for (String userName : avaibleUserConnections.keySet()) {

			user = avaibleUsers.get(userName);
			
			if(user.getMode() == Constants.GAMER_MODE)
				avaibleUserConnections.get(user.getName()).sendMessage(message);
		}
	}

	
	
	
	public void sendUpdateToAllSpectators(UpdateGameMessageForSpectator message) throws IOException{
		
		User user;
		
		for (String userName : avaibleUserConnections.keySet()) {

			user = avaibleUsers.get(userName);
			
			if(user.getMode() == Constants.SPECTATOR_MODE)
				avaibleUserConnections.get(user.getName()).sendMessage(message);
		}
	}
	
	

	
	
	public void startGame(){
		this.gameStarted = true;
		System.out.println("Game started with sucess!");
		//criar conexões com todos os clientes conectados
	}


	public int getRangePrime() {
		return rangePrime;
	}


	public void setRangePrime(int rangePrime) {
		this.rangePrime = rangePrime;
	}


	private TreeMap<String, User> getAvaibleUsers() {
		return avaibleUsers;
	}

	@SuppressWarnings("unchecked")
	public TreeMap<String, User> getListOfAvaibleUsers() {
		return ((TreeMap<String, User>)avaibleUsers.clone());
	}


	public boolean isGameStarted() {
		return gameStarted;
	}


	/**
	 * @return the definedPrimes
	 */
	public TreeMap<String, Long> getDefinedPrimes() {
		return definedPrimes;
	}


	/**
	 * @return the connectedServer
	 */
	public boolean isConnectedServer() {
		return connectedServer;
	}


	/**
	 * @return the changedAvaibleUsers
	 */
	public boolean isChangedAvaibleUsers() {
		return changedAvaibleUsers;
	}
	
	
	public void setChangedAvaibleUsers(boolean value) {
		this.changedAvaibleUsers = value;
	}
	
}
